package com.fbote.media.wav;
import javax.media.ControllerEvent;
import javax.media.ControllerListener;
import javax.media.EndOfMediaEvent;
import javax.media.GainControl;
import javax.media.Manager;
import javax.media.MediaLocator;
import javax.media.Player;
import javax.media.PrefetchCompleteEvent;
import javax.media.RealizeCompleteEvent;
import javax.media.StopEvent;
import javax.media.Time;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;

public class SoundServer {
  private static final int SOCKET_BACKLOG = 1;
  private static final int PORT = 7890;
  private static final String BIND_ADDRESS = "127.0.0.1";
  private static final String PLAY_COMMAND = "play";
  private static final String STOP_COMMAND = "stop";
  private static final String DELIMITER = "\\|";
  private static final float DEFAULT_VOLUME = 0.4f;

  private static final Logger log = Logger.getAnonymousLogger();

  private final Map<String, SoundPlayer> playerMap;

  private SoundServer() {
    playerMap = new ConcurrentHashMap<String, SoundPlayer>();
  }

  private String playSound(String file, float volume) {
    SoundPlayer player = new SoundPlayer(file, volume);
    playerMap.put(file, player);
    player.playSound();
    return "Playing:------------" + file;
  }

  private String stopSound(String file) {
    SoundPlayer player = playerMap.get(file);
    player.stopSound();
    return "Stopped:------------" + file;
  }

  private PrintWriter getWriter(Socket socket) throws IOException {
    OutputStream socketOut = socket.getOutputStream();
    return new PrintWriter(socketOut, true);
  }

  private BufferedReader getReader(Socket socket) throws IOException {
    InputStream socketIn = socket.getInputStream();
    return new BufferedReader(new InputStreamReader(socketIn));
  }

  private void start() throws IOException {
    log.info("Sound Server started and bound to port " + PORT);
    ServerSocket serverSocket = null;
    try {
      serverSocket = new ServerSocket(PORT, SOCKET_BACKLOG, InetAddress.getByName(BIND_ADDRESS));

      while (true) {
        Socket socket = null;
        BufferedReader br = null;
        PrintWriter pw = null;
        try {
          socket = serverSocket.accept();
          log.info("New connection accepted" +
            socket.getInetAddress() + ":" + socket.getPort());
          br = getReader(socket);
          pw = getWriter(socket);

          String msg;
          while ((msg = br.readLine()) != null) {
            log.info("Received message: " + msg);
            pw.println(executeInputCommand(msg));
            if (msg.equals("bye"))
              log.info("Stopping sound server");
            break;
          }
        } catch (Throwable e) {
          log.severe(e.toString() + " caught. Continuing sound server.");
        }
        finally {
          try {
            if (br != null) {
              br.close();
            }
          } catch (Exception e) { /* swallow */ }
          try {
            if (pw != null) {
              pw.close();
            }
          } catch (Exception e) { /* swallow */ }
          try {
            if (socket != null) {
              socket.close();
            }
          } catch (Exception e) { /* swallow */ }
        }
      }
    } finally {
      serverSocket.close();
    }
  }

  private String executeInputCommand(String message) {
    log.info("Attempting to execute command received " + message);
    String[] commandArray = parseCommandArray(message);
    if (commandArray.length < 2) {
      log.warning("Received no command or no sound file to play or stop. Ignoring message with contents " + message);
      return "";
    }

    String command = commandArray[0];
    String file = commandArray[1];
    float volume = DEFAULT_VOLUME;

    if (commandArray.length > 2) {
      try {
        float f = Float.parseFloat(commandArray[2]);
        volume = f / 100;
        log.info("Setting volume to " + volume);
      } catch (NumberFormatException nfe) {
        // swallow - ignore and use default
      }
    }

    if (command.equals(PLAY_COMMAND)) {
      return playSound(file, volume);
    } else if (command.equals(STOP_COMMAND)) {
      return stopSound(file);
    }

    log.warning("Unknown command received " + command + " ignoring message " + message);
    return "";
  }

  private String[] parseCommandArray(String message) {
    return message.split(DELIMITER);
  }

  public static void main(String[] args) throws Exception {
    try {
      SoundServer server = new SoundServer();
      server.start();
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  class SoundPlayer implements ControllerListener {
    private Player player;
    private String musicFile;
    private float volume;

    SoundPlayer(String musicFile, float volume) {
      this.musicFile = musicFile;
      this.volume = volume;
    }

    synchronized void playSound() {
      log.info(musicFile + " being played");
      try {
        player = Manager.createPlayer(new MediaLocator("file:" + musicFile));
        player.addControllerListener(this);
        player.prefetch();
      } catch (Exception ex) {
        log.severe("Error caught trying to play sound " + musicFile + " " + ex.toString());
      }
    }

    synchronized void stopSound() {
      log.info("Received request to stop sound: " + musicFile);
      player.stop();
    }

    public void controllerUpdate(ControllerEvent e) {
      if (e instanceof EndOfMediaEvent) {
        player.setMediaTime(new Time(0));
        player.stop();
        log.info(musicFile + " sound stopped");
      } else if (e instanceof PrefetchCompleteEvent) {
        log.info("Got prefetch complete event");
        player.start();
      } else if (e instanceof RealizeCompleteEvent) {
        setVolume();
      } else if (e instanceof StopEvent) {
        player.close();
        SoundServer.this.playerMap.remove(musicFile);
        log.info("Player for " + musicFile + " cleaned up");
      }
    }

    public void setVolume() {
      log.info("Setting volume for " + musicFile + " to " + this.volume);
      GainControl gc = player.getGainControl();
      gc.setLevel(this.volume);
    }
  }
}
